/////////////////////////////////////////////////////////////////////////////
// Purpose:     Remote connections through sockets management -client
// Author:      Jesus Gonzalez
// Modified by:
// Copyright:   (c) 2003-2013 Jesus Gonzalez <jgonzalez@gdr-sistemas.com>
// License:     BSD License
/////////////////////////////////////////////////////////////////////////////

#include "SmartLib/Socket.h"

#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netdb.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>

#include "Log.h"

bool ClientSocket::Connect(struct addrinfo *ai, struct timeval* ts_timeout)
{
    int sktopts = 0;

    // Set socket to non-blocking if timeout is specified
    if( ts_timeout != NULL )
    {
        sktopts = fcntl(m_fd, F_GETFL);
        if( sktopts < 0 )
        {
            THROW_ERROR("Error getting socket option flags (Error=%s)", strerror(errno));
        }

        if( fcntl(m_fd, F_SETFL, sktopts | O_NONBLOCK) < 0 )
        {
            THROW_ERROR("Error setting socket option flags (Error=%s)", strerror(errno));
        }
    }

    bool connected = false;
    while(true)
    {
        // Connect to the server socket
        if( connect( m_fd, ai->ai_addr, ai->ai_addrlen ) < 0 )
        {
            if( errno == EINTR ) continue;

            if( errno != EINPROGRESS )
            {
                LOG_DEBUG("Error connecting socket (Error=%s)", strerror(errno));
                break;
            }

            fd_set fds;
            FD_ZERO( &fds );
            FD_SET(m_fd, &fds );

            int n = 0;
            while(true)
            {
                n = select( m_fd+1, NULL, &fds, NULL, ts_timeout );
                if( n < 0 )
                {
                    if( errno == EINTR ) continue;

                    THROW_ERROR("Error waiting for socket connection (Error=%s)", strerror(errno));
                }
                else if( n > 0 )
                {
                    // Connection is successful if the SO_ERROR socket option is 0
                    int valopt;
                    socklen_t lon = sizeof(valopt);
                    if( getsockopt( m_fd, SOL_SOCKET, SO_ERROR, &valopt, &lon) < 0 )
                    {
                        THROW_ERROR("Error getting socket options (Error=%s)", strerror(errno));
                    }
                    if( valopt != 0 )
                    {
                        LOG_DEBUG("Error connecting socket (Error=%s)", strerror(errno));
                    }
                    else
                    {
                        connected = true;
                    }
                }
                else
                {
                    LOG_DEBUG("Socket connection timeout");
                }

                break;
            }
        }
        else
        {
            connected = true;
        }

        break;
    }

    // Set socket to original options
    if( ts_timeout != NULL )
    {
        if( fcntl(m_fd, F_SETFL, sktopts) < 0 )
        {
            THROW_ERROR("Error setting socket option flags (Error=%s)", strerror(errno));
        }
    }

    return connected;
}

void ClientSocket::Connect( const std::string &hostname, const std::string &port, int timeout )
{
	if( m_fd >= 0 )
	{
		THROW_ERROR("Socket is already connected to a host");
	}

	// Prepare timeout data
    struct timeval* ts_timeout = NULL;
    struct timeval _ts_timeout;
    if( timeout >= 0 )
    {
        _ts_timeout.tv_sec = (timeout / 1000);
        _ts_timeout.tv_usec = (timeout % 1000)*1000;
        ts_timeout = &_ts_timeout;
    }

    // TODO: socket configuration modifiers
	struct addrinfo hints;
	memset( &hints, 0, sizeof(hints) );
	hints.ai_family = AF_UNSPEC;   		// Allow IPv4/6
	hints.ai_socktype = SOCK_STREAM;	// Stream socket
	hints.ai_protocol = IPPROTO_TCP;	// TCP

	struct addrinfo *base_ai = NULL;
	int res = getaddrinfo( hostname.c_str(), port.c_str(), &hints, &base_ai );
	if( res != 0 )
	{
		THROW_ERROR( "Couldn't resolve host address '%s' (errno = %s)", hostname.c_str(), gai_strerror(res) );
	}

	struct addrinfo *curr_ai = NULL;
	for( curr_ai = base_ai; curr_ai != NULL; curr_ai = curr_ai->ai_next )
	{
		// Create the TCP socket
		m_fd = socket( curr_ai->ai_family, curr_ai->ai_socktype, curr_ai->ai_protocol );
		if( m_fd < 0 ) continue;

        if( Connect(curr_ai, ts_timeout) ) break;

		close( m_fd );
		m_fd = -1;
	}

	freeaddrinfo(base_ai);

	if( !curr_ai )
	{
		// No address succeeded
		THROW_ERROR("Failed to connect to host '%s:%s'", hostname.c_str(), port.c_str());
	}

	LOG_DEBUG( "Client connected to host '%s:%s' (fd = %d)", hostname.c_str(), port.c_str(), m_fd );
}

void ClientSocket::Connect( const std::string &conStr, int timeout )
{
	// Separate connection string's hostname and port
	size_t sep_pos = conStr.find(':');
	if( sep_pos != std::string::npos )
	{
		std::string port = conStr.substr( sep_pos + 1 );
		std::string hostname = conStr.substr( 0, sep_pos );
		Connect( hostname, port, timeout );
	}
	else
	{
		THROW_ERROR("The server port must be specified ('hostname:port')");
	}
}

void ClientSocket::Connect( const std::string &hostname, uint16 default_port, int timeout )
{
	Connect( hostname, FormatString( "%d", default_port ), timeout );
}

bool ClientSocket::isConnected() const
{
    return (m_fd >= 0);
}
